(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
如果你在撰寫React專案時,有試著在第一次渲染後,透過useEffect
以state修改綁定給元件的資料,應該會發現一個特殊的現象:
嗯?為甚麼我的元件會閃一下?
以下方的程式為例
import React, {useState,useMemo,useEffect} from 'react';
import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';
let menuItemWording = new Array(20);
menuItemWording.fill("沒有東西");
const MenuPage = () =>{
const [isOpen, setIsOpen] = useState(true);
const [menuItemData, setMenuItemData] = useState(menuItemWording);
let menuItemArr = useMemo(()=>menuItemData.map((wording) => <MenuItem text={wording} key={wording}/>),[menuItemData]);
useEffect(()=>{
setMenuItemData([
"Like的發問",
"Like的回答",
"Like的文章",
"Like的留言"
]);
},[])
return (
<OpenContext.Provider value={{
openContext: isOpen,
setOpenContext: setIsOpen
}} >
<Menu title={"Andy Chang的like"}>
{menuItemArr}
</Menu>
<button onClick={()=>{
let menuDataCopy = ["測試資料"].concat(menuItemData);
setMenuItemData(menuDataCopy);
}}>更改第一個menuItem</button>
</OpenContext.Provider>
);
}
export default MenuPage;
你會得到下列的結果:
這是為什麼呢?
這是因為我們先前說過,useEffect
的執行時間點是這樣:
也就是在第一次渲染時,React還沒有拿到你在useEffect
中修改的state
,所以畫面上顯示的會是你一開始拿來當初始值的資料。這也是我們的畫面會閃一下的原因。
(2020/10/11) 但如果你是從鐵人賽一開始就在跟我文章的人,我當初這裡有不小心寫錯。現在已經修正了。雖然這種人應該不多就是(?)
雖然這種需求很少,但React提供了一個解決上述問題的hook-useLayoutEffect
。它和useEffect
的語法、使用上一模一樣。唯一的差別是useLayoutEffect
被提升到了渲染畫面前、更新DOM後執行。
useLayoutEffect
的執行時間點是這樣:
接下來你可以試著把剛剛的useEffect
換成useLayoutEffect
。
import React, {useState,useMemo,useLayoutEffect} from 'react';
import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';
let menuItemWording = new Array(20);
menuItemWording.fill("沒有東西");
const MenuPage = () =>{
const [isOpen, setIsOpen] = useState(true);
const [menuItemData, setMenuItemData] = useState(menuItemWording);
let menuItemArr = useMemo(()=>menuItemData.map((wording) => <MenuItem text={wording} key={wording}/>),[menuItemData]);
useLayoutEffect(()=>{
setMenuItemData([
"Like的發問",
"Like的回答",
"Like的文章",
"Like的留言"
]);
},[])
return (
<OpenContext.Provider value={{
openContext: isOpen,
setOpenContext: setIsOpen
}} >
<Menu title={"Andy Chang的like"}>
{menuItemArr}
</Menu>
<button onClick={()=>{
let menuDataCopy = ["測試資料"].concat(menuItemData);
setMenuItemData(menuDataCopy);
}}>更改第一個menuItem</button>
</OpenContext.Provider>
);
}
export default MenuPage;
你會發現畫面不再會閃過一次初始的資料了。這是因為React在第一次渲染畫面前已經執行了useLayoutEffect
中的setState
。
然而必須要注意的事情是,useLayoutEffect
本身是一個同步函式,也就是說UI會等useLayoutEffect
中做的事情結束才會渲染。所以不要在useLayoutEffect
做太多事情,否則使用者看到UI的間隔會拉長,導致UX變差。
這件事衍伸的問題是,當你要在React做SSR時,因為
useLayoutEffect
和usetEffect
都不會在server-side執行,有需要useLayoutEffect
的元件就可能會以不符你的預期的方式運作。
useEffect
都應該能夠解決你的問題。如果你是從class component切換過來的人,實質上useLayoutEffect的執行時機點才是真正等於componentDidMount和componentDidUpdate。 但使用上官方還是希望你使用useEffect。
參考資料:
React Hooks 一些紀錄
官方文件
想問關於 useLayoutEffect 因為是在畫面 render 前執行的
所以像 fetch data 之類串接 API 的動作 是不是也是搬到 useLayoutEffect 中執行會比較好呢 謝謝
Hi, 正好相反喔!雖然這邊說useLayoutEffect是同步且在畫面繪製前執行的,但是call http request本身是Javascript的非同步行為,呼叫後會走event loop的流程。而React本身運作是在Javasciprt主thread中,所以不會等API reponse回來再去繪製畫面。
會用到useLayoutEffect的情境主要是「UI和Client side Javascript運算有關」。如果UI是和Server side有關(需要後端資料等等),有兩種解法:
另外要注意的是useLayoutEffect並不會在server side執行,所以如果你的專案需要後端資料又真的不允許讀取圖示的這段空檔,就應該要使用像Next.js這類的方案,使用useLayoutEffect並不能解決這個問題
哦哦哦 原來如此!
了解了 非常感謝!